#include <cmath>
#include <cstdlib>
#include <ctime>
#include <functional>
#include <set>
#include <vector>
#include <iomanip>
#include <list>
#include "main\preprocess.h"

const int USER_NUM = 100000;
const int MOVIE_NUM = 100000;
const int TOTAL_FEATURES = 150;

struct MovieRateNode
{
	double	Rate;     
	int		MovieID;  
};

typedef std::vector< std::vector< MovieRateNode > > RateMatrixType; 
RateMatrixType  RateMatrix( USER_NUM + 1 );
std::set< int >	UserSet;

double bi[ MOVIE_NUM + 1 ] = { 0 }; 
double bu[ USER_NUM + 1 ] = { 0 }; 
double p[ USER_NUM + 1 ][ TOTAL_FEATURES + 1 ] = { 0 };
double q[ MOVIE_NUM + 1 ][ TOTAL_FEATURES + 1 ] = { 0 }; 
int biNum[ MOVIE_NUM + 1 ] = { 0 };
int buNum[ USER_NUM + 1 ] = { 0 }; 
double factor = 2.109;
double mean = 0; 

void LoadFile( const std::string& filename, std::function< void( std::stringstream& file ) > func )
{
	std::stringstream file;
	Preprocess( filename, file );

	while ( ! file.eof() )	func( file );
}

double DotProduct( double * p, double * q, int dim )
{
	double result = 0.0;
	for ( int i = 1; i < dim + 1; i++ )
	{
		result += p[ i ] * q[ i ];
	}
	return result;
}

void MakeRand( double p [], int dim )
{
	for ( int i = 1; i < dim + 1; i++ )
	{
		//p[ i ] = 1.0 * ( rand() / (double)RAND_MAX / sqrt( (double)dim ) );

		// http://www.netflixprize.com//community/viewtopic.php?pid=6778#p6778
		const double INIT_SEED = sqrt( 0.10 / (double)dim );   
		const double INIT_VARIANCE = 0.15;        
		const double INIT = (INIT_SEED + ( 2.0 * ( rand() / (float)(RAND_MAX) ) - 1.0 ) * INIT_VARIANCE);
		p[ i ] = INIT;
	}
}

void InitPQ( int itemNum, int userNum, int dim )
{
#pragma omp parallel num_threads( 2 )
	{
#pragma omp for
		for ( int i = 1; i < itemNum + 1; i++ )
		{
			MakeRand( q[ i ], dim );
		}

#pragma omp for
		for ( int i = 1; i < userNum + 1; i++ )
		{
			MakeRand( p[ i ], dim );
		}
	}

}	

double GetMeanRating()
{
	double sum = 0;
	int num = 0;
	for ( std::size_t u = 0; u < RateMatrix.size(); u++ )
	{
		for ( std::size_t i = 0; i < RateMatrix[ u ].size(); i++ )
		{
			sum += RateMatrix[ u ][ i ].Rate;
			num++;
		}
	}

	return sum / num;
}

double round(double number)
{
	return number < 0.0 ? ceil(number - 0.5) : floor(number + 0.5);
}

double _PredictRate( int userID, int MovieID, int dim )
{	
	double ret = mean + bu[ userID ] + bi[ MovieID ] 
	+ ( RateMatrix[ userID ].size() > 1 ? DotProduct( p[ userID ], q[ MovieID ], dim ) : 0 );

	if ( ret < 0.5 * factor ) ret = 0.5 * factor;
	else if ( ret > 5 * factor ) ret = 5 * factor;

	return ret;
}

void BuildModel( int dim, double alpha1, double alpha2,
	double beta1, double beta2, int maxStep, 
	double slowRate )
{
	mean = GetMeanRating();

	srand( (unsigned)time( 0 ) );
	InitPQ( MOVIE_NUM, USER_NUM, TOTAL_FEATURES );

	for ( int step = 0; step < maxStep; ++step )
	{
		for ( std::set< int >::iterator iter = UserSet.begin(); iter != UserSet.end(); ++iter )
		{
			int u = *iter;
			for ( std::size_t i = 0; i < RateMatrix[ u ].size(); i++ )
			{
				int MovieID	= RateMatrix[ u ][ i ].MovieID;
				double rui	= RateMatrix[ u ][ i ].Rate;
				double pui	= _PredictRate( u, MovieID, dim );
				double eui	= rui - pui;		

				bu[ u ] += alpha1 * ( eui - beta1 * bu[ u ] );
				bi[ MovieID ] += alpha1 * ( eui - beta1 * bi[ MovieID ] );

				//#pragma omp parallel for num_threads( 2 )
				for ( int k = 1; k < TOTAL_FEATURES + 1; k++ )
				{
					p[ u ][ k ] += alpha2 * ( eui * q[ MovieID ][ k ] - beta2 * p[ u ][ k ] );
					q[ MovieID ][ k ] += alpha2 * ( eui * p[ u ][ k ] - beta2 * q[ MovieID ][ k ] );
				}
			}
		}

		alpha1 *= slowRate;
		alpha2 *= slowRate;
		std::cout << ".";
	}
}

double PredictRate( int userID, int MovieID, int dim )
{
	return int( round( _PredictRate( userID, MovieID, dim ) / factor * 2.0 ) ) / 2.0;
}

struct TestSetNode
{
	int		userID;
	int		movieID;
	double	rate;
};

std::list< TestSetNode > TestSet;

void CalMAEandRMSE( int dim )
{
	std::ofstream out( "out.txt" );
	std::size_t size = TestSet.size();
	long double rmse = 0, mae = 0;

	for ( std::list<TestSetNode>::iterator iter = TestSet.begin(); iter != TestSet.end(); ++iter )
	{
		double pRate = PredictRate( iter->userID, iter->movieID, dim );

		double err = pRate - iter->rate;
		rmse += err * err;
		mae += fabs( err );

		out << std::setiosflags( std::ios::left ) << std::setw( 8 ) << iter->userID << std::setw( 8 ) << iter->movieID 
			<< std::setw( 8 ) << pRate << std::setw( 6 ) << iter->rate << std::endl;
	}

	rmse = sqrt( rmse / size );
	mae /= size;

	out << "\nmae = " << mae << "\nrmse = " << rmse << std::endl;
	std::cout << "\nmae = " << mae << "\nrmse = " << rmse << std::endl;
}

int main()
{
	time_t start = time(NULL);

	LoadFile( "trainSetTemp.txt", []( std::stringstream& file ){ int userID; MovieRateNode node; file >> userID >> node.MovieID >> node.Rate; node.Rate *= factor; RateMatrix[ userID ].push_back( node ); UserSet.insert( userID ); } );

	BuildModel( TOTAL_FEATURES, 0.01, 0.01, 0.05, 0.05, 500, 0.99 ); 

	LoadFile( "testSetTemp.txt", []( std::stringstream& file ){ TestSetNode node; file >> node.userID >> node.movieID >> node.rate; TestSet.push_back( node );} );

	CalMAEandRMSE( TOTAL_FEATURES );

	std::cout << "Use: " << double( time(NULL) - start ) << " s!" << std::endl;
	
	return 0;
}
